package edu.unika.aifb.graphindex.searcher.keyword.evaluation;

import java.util.HashMap;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import edu.unika.aifb.graphindex.model.IEntity;
import edu.unika.aifb.graphindex.model.impl.Entity;
import edu.unika.aifb.graphindex.query.QNode;
import edu.unika.aifb.graphindex.query.QueryEdge;
import edu.unika.aifb.graphindex.query.QueryGraph;

public class QueryExecution implements Comparable<QueryExecution> {
	private TopKQueryEvaluator topKQueryEvaluator;
	private QueryGraph queryGraph;
	private PriorityQueue<Row> candidateRows;
	private double threshold;
	private SortedSet<Row> resultRows;
	private Set<QNode> nodes;
	private Set<QNode> keywordNodes;
	private Iterator<QNode> keywordNodeIterator;
	private boolean finished = false;
	
	public QueryExecution(TopKQueryEvaluator topKQueryEvaluator, QueryGraph queryGraph){
		this.topKQueryEvaluator = topKQueryEvaluator;
		this.queryGraph = queryGraph;
		
		nodes = queryGraph.vertexSet();
		keywordNodes = queryGraph.getKeywordNodes();
		keywordNodeIterator = keywordNodes.iterator();
		candidateRows = new PriorityQueue<Row>();
		candidateRows.add(new Row(nodes, queryGraph));
		resultRows = new TreeSet<Row>();
		
		updateThreshold();
	}
	
	public SortedSet<Row> execute(int topK){
		while((resultRows.size() < topK) && !candidateRows.isEmpty() ){
			if(resultRows.size() == topK)
				break;
			
			Row candidateRow = candidateRows.poll();
		
			if(!finished)
				finished = !hasMoreUnseenEntities();
			
			if(!finished && (candidateRow.getScore() <= threshold)){
				if(candidateRow.getLastNode() != null)
					candidateRows.add(candidateRow);
				
				candidateRow = new Row(nodes, queryGraph);
				
				QNode node = chooseNextNodeToVisit();
				IEntity entity = node.getNextEntity();
				updateThreshold(node);
				
				candidateRow.addEntity(node, entity);
				candidateRows.add(candidateRow);
			}else{
				if(candidateRow.isComplete()){
					resultRows.add(candidateRow);
				}
				else{
					QNode lastNode = candidateRow.getLastNode();
					QNode nextNode = candidateRow.getNextNode();
					QueryEdge queryEdge = queryGraph.getEdge(lastNode, nextNode);
					
					if(queryEdge == null)
						queryEdge = queryGraph.getEdge(nextNode, lastNode);
					
					String entity = candidateRow.getNodeToEntity().get(lastNode).getUri();
					
					Set<String> connectedEntities = null;
					
					if(lastNode == queryEdge.getSource()){
						connectedEntities = topKQueryEvaluator.getTargetEntities(entity, queryEdge.getLabel());
					}else {
						connectedEntities = topKQueryEvaluator.getSourceEntities(entity, queryEdge.getLabel());
					}
					
					Row newCandidateRow = candidateRow;
					for(String connectedEntityUri : connectedEntities){
						if(connectedEntities.size() > 1)
							newCandidateRow = candidateRow.clone();
						
						IEntity connectedEntity = null;
						
						if(!nextNode.isKeywordNode()){
							connectedEntity = new Entity(connectedEntityUri);
							if(nextNode.getMaxScore() > 0){
								topKQueryEvaluator.setEntityScore(connectedEntity);
							}
						}else{
							connectedEntity = nextNode.getEntity(connectedEntityUri);
						}
						
						if(connectedEntity != null){
							newCandidateRow.addEntity(nextNode, connectedEntity);
							candidateRows.add(newCandidateRow);
						}
					}
				}
			}
		}
		return resultRows;
	}
	
	private boolean hasMoreUnseenEntities(){
		for(QNode keywordNode : keywordNodes)
			if(keywordNode.hasMoreEntities())
				return true;
		return false;
	}
	
	private void updateThreshold(){
		HashMap<QNode, IEntity> nodeToEntity = new HashMap<QNode, IEntity>();
		
		for(QNode node : nodes){
			nodeToEntity.put(node, node.getTopEntity());
		}
		threshold = CombineScore.combineEntityScore(nodeToEntity);
	}
	
	private void updateThreshold(QNode node){
		HashMap<QNode, IEntity> nodeToEntity = new HashMap<QNode, IEntity>();
		nodeToEntity.put(node, node.getCurrentEntity());
		
		for(QNode qnode : keywordNodes){
			if(qnode != node){
				nodeToEntity.put(qnode, qnode.getTopEntity());
			}
		}
		
		// Nodes : a,b,c ; currentNode a;
		// Threashold - a.currentEntity.score + b.topEntity.score + c.topEntity.score
		threshold = CombineScore.combineEntityScore(nodeToEntity);
		for(QNode qnode : keywordNodes){
			if(qnode != node){
				nodeToEntity = new HashMap<QNode, IEntity>();
				
				IEntity currentEntity = qnode.getCurrentEntity();
				if(currentEntity != null){
					if(qnode.hasMoreEntities())
						nodeToEntity.put(qnode, currentEntity);
					else{
						continue;
					}
				}else{
					nodeToEntity.put(qnode, qnode.getTopEntity());
				}
				
				for(QNode qnode2 : keywordNodes){
					if(qnode2 != qnode)
						nodeToEntity.put(qnode2, qnode2.getTopEntity());
				}
				// Threashold - a.topEntity.score + b.currentEntity.score + c.topEntity.score
				// Threashold - a.topEntity.score + b.topEntity.score + c.currentEntity.score
				threshold = Math.max(threshold, CombineScore.combineEntityScore(nodeToEntity));
			}
		}
	}
	

	
	// Round Robin
	private QNode chooseNextNodeToVisit(){
		if(keywordNodeIterator.hasNext()){
			QNode node = keywordNodeIterator.next();
			if(node == null || !node.hasMoreEntities())
				return chooseNextNodeToVisit();
			else
				return node;
		}
		keywordNodeIterator = keywordNodes.iterator();
		return chooseNextNodeToVisit();
	}
	
	public double getThreshold(){
		return threshold;
	}
	
	public boolean isFinished(){
		return finished && candidateRows.size() == 0;
	}
	
	public QueryGraph getQueryGraph() {
		return queryGraph;
	}

	public void setQueryGraph(QueryGraph queryGraph) {
		this.queryGraph = queryGraph;
	}
	
	public SortedSet<Row> getResultRows(){
		return resultRows;
	}

	@Override
	public int compareTo(QueryExecution queryExecution){
		if(this.getThreshold() > queryExecution.getThreshold())
			return -1;
		if(this.getThreshold() < queryExecution.getThreshold())
			return 1;
		return 0;
	}
	
	public String toString(){
		return getThreshold() + " " + queryGraph.toString();
	}
}
